#!/usr/bin/env python

import re
import os

import gtk
import glib
import pygtk

import threading
import copy
import gobject

import balancer

import yaml

import uuid

import smtplib

import balancer.ui.windows
import balancer.history
import balancer.ui.config
import balancer.listeners
import balancer.scheduler
import balancer.log

  
class BalanceAccountConfig:
    """The credentials and other information describing a specific instance of an account."""

    def __init__(self, my_uuid):
        """The UUID is a sequence of bytes unique to this account."""
        self.__uuid = my_uuid

        self.__attrs = None
        self.__secrets = None

        self.__type = None

        self.__name = None

    def set_type(self, type):
        """Set the loader for this store."""
        self.__type = type

    def get_name(self):
        return self.__name

    def get_type(self):
        """Get the loader for this store."""
        return self.__type

    def save(self, name, attrs, secrets, progress):
        self.__name = name
        self.__attrs = attrs
        self.__secrets = secrets

    def load(self, progress):
        return (self.__attrs, self.__secrets, self.__name)

    def get_uuid(self):
        return self.__uuid


class GuiManager:
    """Handles the the gui actions."""
    
    def __init__(self):
        self.__log = balancer.log.Log(self)
        self.__home = os.path.expanduser('~') + "/.balancer/"
        self.__passphrase = None
        self.__scheduler = balancer.scheduler.DayOfWeekScheduler(
                self, lambda: self.__cb_refresh(None))

        self.__historian = balancer.history.Historian(self)

        self.__refresh_listeners = [
                balancer.listeners.HistoryRefreshListener(self),
                balancer.listeners.SmsRefreshListener(self),
        ]


    def initialize(self, icon):
        """Initialize the app's GUI"""

        gtk.window_set_default_icon_list(*balancer.ui.resources.icon_list(balancer.ui.resources.BASE_ICON))

        # Verify that we have a config directory
        if os.path.exists(self.__home):
            if not os.path.isdir(self.__home):
                icon.set_error("Need a directory at " + self.__home)

        else:
            os.makedirs(self.__home, 0700)

        self.load_balancers()
        self.load_config()
        self.load_credentials()
    
        self.start(icon)

        def pass_cb(): pass

        if balancer.has_keyring():
            d = balancer.ui.windows.PassphraseEntryDialog()

            def reset():
                balancer.reset()
                balancer.ui.windows.StartupWizard().run(balancer.create_keyring, pass_cb, pass_cb)
            
            d.run(balancer.unlock, reset)
        else:
            balancer.ui.windows.StartupWizard().run(balancer.create_keyring, pass_cb, pass_cb)


    def get_home(self):
        """Returns the path to the existing config directory."""
        return self.__home


    def load_balancers(self):
        """Load the balancer plugins"""


    def load_config(self):
        """Load the user's config"""
        try:
            self.__config = yaml.load(open(self.__home + "config.yaml"), yaml.SafeLoader)
        except IOError:
            self.__config = {'config_1.0' : {
                'accounts' : {},
                'notification' : {
                    'email' : None, # Email address to notify when something happens
                    'schedule' : None # Schedule for when notification should run
                }
            }}

        self.__scheduler.load_config(self.__config['config_1.0']['notification']['schedule'])


    def load_credentials(self):
        """Load the user's stored credentials"""
        try:
            self.__cred = yaml.load(open(self.__home + "credentials.yaml"), yaml.SafeLoader)
        except IOError:
            self.__cred = {'credentials_1.0' : {}}


    def save_credentials(self):
        """Save the user's credentials."""
        f = open(self.__home + 'credentials.yaml', 'w')
        try:
            yaml.dump(self.__cred, f)
        except IOError as e:
            print e
        finally:
            f.close()


    def save_config(self):
        """Save the config."""
        f = open(self.__home + 'config.yaml', 'w')
        try:
            yaml.dump(self.__config, f)
        except IOError as e:
            print e
        finally:
            f.close()

    def save_schedule(self, schedule):
        self.__config['config_1.0']['notification']['schedule'] = schedule
        self.save_config()
        

    def get_historian(self):
        return self.__historian


    def __cb_add_account(self, widget):
        """Add an account"""
        wizard = balancer.ui.windows.AddAccountWizard(self, BalanceAccountConfig(uuid.uuid4().hex))
        wizard.run(self.__cb_save_account)


    def __cb_save_account(self, acct_config):
        """Save the given BalanceAccountConfig."""
        # Create the credential store
        store = balancer.CredentialStore2()
        (attrs, creds, name) = acct_config.load(balancer.Progress())

        attrs = {
            "uuid" : acct_config.get_uuid(),
            "loader" : acct_config.get_type().get_source_uuid(),
            "name" : acct_config.get_name()
        }

        store.save(acct_config.get_name(), attrs, creds, balancer.Progress())

    def __cb_refresh(self, widget):
        """Refresh the accounts"""
        runner = BalanceRefreshRun(self)
        for cred in balancer.credentials(balancer.Progress()):
            (attrs, secrets, name) = cred.load(balancer.Progress())
            type = None
            src_uuid = None
            try:
                type = self.get_balancer(attrs['loader'])
                src_uuid = attrs['uuid']
            except KeyError:
                print "Credential failure"

            runner.add(src_uuid, type, cred)

        runner.run(self.__inform_refresh_listeners, None)


    def __inform_refresh_listeners(self, results):
        """Called when a __cb_refresh() is complete."""
        desc = balancer.listeners.RefreshDescription()
        for listener in self.__refresh_listeners:
            listener.refreshed(results, desc)


    def __cb_show_balances(self):
        """Show the balances"""


    def __cb_configure(self, menu):
        """Show the configuration dialog."""
        d = balancer.ui.config.ConfigDialog(self)
        d.run()


    def __cb_exit(self, menu):
        """Quit the app."""
        gtk.main_quit()
        # Erm, now what?


    def set_email_notification(self, address, server, port, fromAddy, password):
        """Set the email notification address to the passed-in item"""
        if not self.__config['config_1.0']['notification']:
            self.__config['config_1.0']['notification'] = {}

        self.__config['config_1.0']['notification']['email'] = {
            'to' : address,
            'from' : fromAddy,
            'server' : server,
            'port' : port,
            'password' : password
        }

        self.save_config()


    def set_email_notification_filter(self, filter):
        if not self.__config['config_1.0']['notification']:
            self.__config['config_1.0']['notification'] = {}

        self.__config['config_1.0']['notification']['filter'] = filter.to_save_structure()
        

        self.save_config()

    def get_email_notification_filter(self):
        f = {}
        if self.__config['config_1.0']['notification'].has_key('filter'):
            f = self.__config['config_1.0']['notification']['filter']

        return balancer.BalanceFilter().become_save_structure(f)


    def get_email_notification(self):
        return self.__config['config_1.0']['notification']['email']


    def start(self, icon):
        """Start running."""
        # Add callbacks to the tray
        icon.set_add_account_callback(self.__cb_add_account)
        icon.set_refresh_callback(self.__cb_refresh)
        icon.set_show_callback(self.__cb_show_balances)
        icon.set_configure_callback(self.__cb_configure)
        icon.set_exit_callback(self.__cb_exit)


    def get_balancers(self):
        """Return the known balancers."""
        return [balancer.ScotiaBankBalanceSource()]


    def get_balancer(self, sourceUuid):
        """Get a balancer by its UUID"""
        if sourceUuid == "ScotiaBank":
            return balancer.ScotiaBankBalanceSource()

        return None


    def get_editor_for(self, type, complete, account):
        """Get a widget responsible for editing/changing account stuff
            complete - A callback to run when the widget is finished. 
            account - A BalanceAccountConfig object to be set. When complete(True) is called, values must have been set in this object.
        """
        
        if isinstance(type, balancer.ScotiaBankBalanceSource):
            return ScotiaBankBalanceSourceUi(type, complete, account)

        return None


    def get_editors(self, complete, account):
        """Get all of the editors.
            complete - A callback to run when the widget is finished. 
            account - A BalanceAccountConfig object to be set. When complete(True) is called, values must have been set in this object.
        """
        return [ScotiaBankBalanceSourceUi(balancer.ScotiaBankBalanceSource())]
    

    def get_scheduler(self):
        return self.__scheduler

    def log(self):
        """Return the event log."""
        return self.__log

class BalanceRefreshRun:
    """A refresh of multiple types."""

    def __init__(self, gm):
        self.__to_refresh = []
        self.__gm = gm

    def add(self, uuid, type, cred):
        """Add the item to refresh"""
        self.__to_refresh.append([uuid, type, cred])

    def run(self, complete_cb, progress):
        """Run the refresh on each item. Note that the complete_cb is called on a newly created thread."""
        self.__complete_cb = complete_cb

        self.__mutex = threading.Lock()
        self.__running_threads = len(self.__to_refresh)
        self.__result = balancer.Balance()

        for row in self.__to_refresh:
            threading.Thread(target=self.__run_row, args=(row,)).start() 

    def __run_row(self, row):
        """Run a type/cred pair. Called from non-UI thread."""
        (uuid, type, cred) = row
        balance = type.load(cred, balancer.Progress(), rate=balancer.BalanceSource.FAST, log=self.__gm.log())

        self.__mutex.acquire()
        try:
            self.__result.devour(balance)
            self.__running_threads -= 1
            if self.__running_threads == 0:
                # We're the last thread. Run the callback.
                self.__complete_cb(self.__result)
        finally:
            self.__mutex.release()


class ScotiaBankBalanceSourceUi:
    """Responsible for generating the editor for a BalanceSource."""

    def __init__(self, type, complete_cb, account):
        self.__type = type
        self.__complete_cb = complete_cb
        self.__account = account
        self.__cred = [None, None]

        self.__card_ok = False
        self.__password_ok = False

        self.__editor = None

    def __update_on_change(self):
        return self.__complete_cb(self.__password_ok and self.__card_ok)

    def get_type(self):
        return self.__type

    def done(self):
        self.__account.save("Bank of Nova Scotia credentials", {}, self.__cred, None)

    def widget(self):
        """Generate a widget that can edit the given account."""
        if self.__editor:
            return self.__editor

        self.__editor = editor = gtk.VBox()

        table = gtk.Table(3, 2)

        # Card number
        label = gtk.Label("ScotiaCard number")
        table.attach(label, 0, 1, 0, 1, xoptions=gtk.FILL)

        entry = gtk.Entry()
        table.attach(entry, 1, 2, 0, 1)
        
        self.__error_label = cardError = gtk.Label("") # Vertical padding
        table.attach(cardError, 0, 2, 1, 2)

        denumPattern = re.compile(r"[^-$.01-9]")
        def cardChanged(widget):
            self.__card_ok = False
            widget.set_text(denumPattern.sub('', widget.get_text()))

            l = widget.get_text_length()
            err = ""
            if l < 16:
                err = "Too short"
            elif l > 16:
                err = "Too long"
            else:
                self.__card_ok = True
                self.__cred[0] = widget.get_text()

            cardError.set_text(err)

            self.__update_on_change()

        entry.connect('changed', cardChanged)


        # Credentials
        label = gtk.Label("Password")
        table.attach(label, 0, 1, 2, 3, xoptions=gtk.FILL)

        pw1 = gtk.Entry()
        table.attach(pw1, 1, 2, 2, 3)
        
        label = gtk.Label("Password\n(confirm)")
        table.attach(label, 0, 1, 3, 4, xoptions=gtk.FILL)

        pw2 = gtk.Entry()
        table.attach(pw2, 1, 2, 3, 4)
        
        pairError = gtk.Label("")
        table.attach(pairError, 0, 2, 4, 5)

        def eq1(widget):
            self.__password_ok = False
            if pw1.get_text() == pw2.get_text():
                if pw1.get_text_length() == 0:
                    pairError.set_text("Passwords may not be empty")
                else:
                    pairError.set_text("")
                    self.__password_ok = True
                    self.__cred[1] = pw1.get_text()
            else:
                pairError.set_text("Passwords do not match")

            self.__update_on_change()
        
        pw1.connect('changed', eq1)
        pw2.connect('changed', eq1)

        editor.pack_start(table)

        return editor

    def set_validation_exception(self, exception):
        """Tell the editor that validation failed."""
        def n():
            self.__error_label.set_text(exception.get_message())

        gobject.idle_add(n)


if __name__ == "__main__":
    gobject.threads_init()
    glib.set_application_name('balancer')
    icon = balancer.ui.TrayIcon()

    gm = GuiManager()
    gobject.idle_add(gm.initialize, icon)

    gtk.main()
